#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import os
import re
import shlex
import shutil
import subprocess
import uuid
from functools import wraps
from io import TextIOBase
from pathlib import Path
from typing import Union, Callable, Any, Iterable
import itertools

from common.err_code import CommErrCode
from common.const import RpcToolInterface
from common.parse_parafile import get_env_variable
from saphanay.comm.error_code_exception import ErrorCodeException
from common.logger import Logger
from saphanay.comm.constants import AgentConstants, RPCToolConstants
from common.const import FilePath

logger = Logger().get_logger()


def logger_watched(level: int) -> Callable:
    """0 <= level <= 4"""
    level = max(0, min(level, 4))

    def decorate(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            ids = [get_env_variable(k) for k in ["pid", "job_id", "sub_job_id"]]
            logger.info(f"{'-' * (5 - level) * 8}> %s start. pid: %s, job_id: %s, sub_job_id: %s", func.__name__, *ids)
            result = func(*args, **kwargs)
            logger.info(f"{'-' * (5 - level) * 8}> %s end. pid: %s, job_id: %s, sub_job_id: %s", func.__name__, *ids)
            return result
        return wrapper

    return decorate


def run_subprocess(command: str, inp: str = None,
                   stdout: Union[int, TextIOBase] = subprocess.PIPE,
                   stderr: Union[int, TextIOBase] = subprocess.PIPE,
                   timeout: int = 600
                   ) -> subprocess.CompletedProcess:
    """
    默认 10 min 超时。
    :raise ErrorCodeException:
    """
    logger.info(f"Run subprocess: {command}")
    try:
        process = subprocess.run(shlex.split(command, posix=os.name == "posix"), input=inp,
                                 stdout=stdout, stderr=stderr, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        logger.error(f"Timeout exception occurred while run subprocess. {command}")
        raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, command, "Timeout")
    except Exception as e:
        logger.error(f"Exception occurred while running subprocess. {command} {e}", exc_info=1)
        raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, command, e)
    if process.stderr:
        logger.error(f"Error output by running subprocess. {command} {process.stderr}")
        raise ErrorCodeException(CommErrCode.FAILED_EXECUTE_COMMAND, command, process.stderr)
    process.stdout and logger.debug(f"Subprocess return: {process.stdout}")
    return process


def rpctool_api(pid: str, api: RpcToolInterface, param: dict) -> dict:
    """
    :raise ErrorCodeException:
    """
    param_file = Path(AgentConstants.TMP_PATH) / f"{RPCToolConstants.PARAM_FILE_PREFIX}{pid}_{uuid.uuid4()}"
    result_file = Path(AgentConstants.STMP_PATH) / f"{RPCToolConstants.RESULT_FILE_PREFIX}{pid}_{uuid.uuid4()}"
    param_file.write_text(json.dumps(param), encoding="utf-8")
    command = f"{RPCToolConstants.RPC_TOOL_PATH} {api} {param_file} {result_file}"
    try:
        run_subprocess(command)
    except ErrorCodeException as e:
        logger.error("RPCtool failed.")
        raise e
    finally:
        param_file.exists() and param_file.unlink()
    if not result_file.exists():
        logger.warn(f"Rpctool result file not generated. Return empty.")
        return {}
    result = json.loads(result_file.read_text(encoding="utf-8"))
    result_file.unlink()
    return result


def is_path_exist(p: str) -> bool:
    return os.path.exists(p) or os.path.islink(p)


def is_path_in_blacklist(p: str) -> bool:
    return bool(re.match(FilePath.PATH_BLACK_LIST, os.path.realpath(p), re.IGNORECASE))


def remove_paths(paths: Union[str, Iterable[str]]) -> None:
    """
    删除不在路径黑名单中的软链接、文件、目录或它们的序列。
    :raise ErrorCodeException:
    """
    if not isinstance(paths, Iterable):
        paths = [paths]
    paths, ignores = list(filter(is_path_exist, paths)), list(itertools.filterfalse(is_path_exist, paths))
    ignores and logger.info(f"Removing paths not exist, ignored. {ignores}")
    if not paths:
        return
    paths, ignores = list(itertools.filterfalse(is_path_in_blacklist, paths)), list(filter(is_path_in_blacklist, paths))
    ignores and logger.info(f"Removing paths in black list, ignored. {ignores}")
    if not paths:
        return
    exception = None
    removed, unremoved = [], []
    for p in paths:
        try:
            os.remove(p) if os.path.islink(p) or os.path.isfile(p) else shutil.rmtree(p)
            removed.append(p)
        except Exception as e:
            logger.error(f"Exception occurred while removing path. {p} {e}", exc_info=1)
            exception = ErrorCodeException(CommErrCode.OPERATION_FAILED)
            unremoved.append(p)
    removed and logger.info(f"Removed. {removed}")
    unremoved and logger.warning(f"UnRemoved. {unremoved}")
    if exception:
        raise exception


def copy_anypath(src: str, dst: str) -> None:
    """
    复制 文件->文件、文件->文件夹、文件夹->文件夹。\n
    覆盖已存在的目标路径。\n
    会创建目录树。
    :raise ErrorCodeException:
    """
    try:
        if os.path.isfile(src):
            shutil.copy2(src, dst)
        elif os.path.isdir(src):
            shutil.copytree(src, dst, dirs_exist_ok=True)
        else:
            logger.error(f"Invalid path, is not file or dir.")
            raise ErrorCodeException(CommErrCode.OPERATION_FAILED)
    except Exception as e:
        logger.error(f"Exception occurred while copying. {src} -> {dst}. {e}", exc_info=1)
        raise ErrorCodeException(CommErrCode.OPERATION_FAILED)
    logger.info(f"Copied. {src} -> {dst}.")

